﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using JBeamLibrary.Exceptions;
using JBeamLibrary.Tools;
using JBeamLibrary.Types;
using JBeamLibrary.VehicleParts.Elements;
using JBeamLibrary.VehicleParts.Enums;
using JBeamLibrary.VehicleParts.Groups;
using Newtonsoft.Json.Linq;

namespace JBeamLibrary.VehicleParts
{
    public class VehicleFile : KeyedCollection<string, VehiclePart>
    {
        #region Fields

        private readonly string _name;
        private readonly Vehicle _parent;
        private readonly JBeam _jbeam;

        #endregion

        #region Properties

        /// <summary>Gets the name of this file</summary>
        public string Name { get { return _name; } }

        /// <summary>Gets the vehicle this part belongs to</summary>
        public Vehicle Parent { get { return _parent; } }

        /// <summary>Gets the file name</summary>
        public string FileName
        {
            get { return string.Format("{0}.jbeam", Name); }
        }

        /// <summary>Gets the path to the vehicle file</summary>
        public string FilePath
        {
            get { return string.Format(@"{0}{1}", Parent.Folder, FileName); }
        }

        /// <summary>Gets a value indicating whether this file is the main vehicle file or not</summary>
        public bool IsMainFile
        {
            get { return Name.SafeEquals(Parent.Name); }
        }

        #endregion

        #region Indexers

        #endregion

        #region CTORs

        public VehicleFile(string fileName, Vehicle parent)
        {
            _name = fileName;
            _parent = parent;
            _jbeam = new JBeam(FilePath);
        }

        #endregion

        #region Methods

        public bool AddPart(string partName)
        {
            if (Contains(partName)) return false;
            Add(new VehiclePart(partName, this));
            return true;
        }

        public bool RemovePart(string partName)
        {
            return Contains(partName) && Remove(partName);
        }

        /// <summary>Loads the vehicle part contents</summary>
        /// <returns></returns>
        public bool Load()
        {
            /* Read file contents */
            if (!File.Exists(_jbeam.Filepath)) return false;
            var root = _jbeam.LoadJbeam();

            /* Build elements */
            foreach (var token in root.Children<JProperty>())   //.Where(token => token.Type == JTokenType.Property)
            {
                /* Build a vehicle part - Skip any other stuff that would happen to be here */
                ExtractPart(token);
            }

            return true;
        }

        /// <summary>Saves the vehicle part contents</summary>
        /// <returns></returns>
        public bool Save(bool compress = false)
        {
            /* Create directory if needed */
            var dir = Path.GetDirectoryName(_jbeam.Filepath);
            if (string.IsNullOrEmpty(dir)) return false;
            if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

            /* Build JBeam */
            var patterns = new List<string>();
            _jbeam.ClearJBeam();
            foreach (var part in this)
            {
                string pattern;
                string replacement;

                BuildPart(part, out pattern, out replacement);

                patterns.Add(pattern);
                patterns.Add(replacement);
            }

            /* Save to file */
            if (string.IsNullOrEmpty(_jbeam.Filepath)) return false;
            // TODO enable patterns when ready => _jbeam.SaveJbeam(compress, patterns.ToArray());
            _jbeam.SaveJbeam(compress);
            return true;
        }

        /// <summary>Saves the contents to a different file</summary>
        /// <param name="filepath"></param>
        /// <param name="compress"></param>
        /// <returns></returns>
        public bool SaveAs(string filepath, bool compress = false)
        {
            _jbeam.Filepath = filepath;
            return Save(compress);
        }

        #endregion

        #region Private methods

        #region Building

        /// <summary>Builds a vehicle part JBeam</summary>
        /// <param name="part"></param>
        /// <param name="pattern"></param>
        /// <param name="replacement"></param>
        private void BuildPart(VehiclePart part, out string pattern, out string replacement)
        {
            /* Build information */
            var propInformation = BuildPartInformation(part);

            /* Build slot type */
            var propSlotType = new JProperty("slotType", part.SlotType);

            /* Build slots */
            var propSlots = BuildPartSlots(part);

            /* Build drag coef */
            JProperty propDragCoef = null;
            if (part.DragCoef > float.MinValue)
            {
                propDragCoef = new JProperty("dragCoef", part.DragCoef.ToJBeam());
            }

            /* Build node scales */
            JProperty propScaleNodeWeight = null;
            if (part.NodeScales != null)
            {
                if (part.NodeScales.WeightScale > float.MinValue)
                    propScaleNodeWeight = new JProperty("scalenodeWeight", part.NodeScales.WeightScale.ToJBeam());
            }

            /* Build beam scales */
            JProperty propScaleBeamSpring = null;
            JProperty propScaleBeamDamp = null;
            JProperty propScaleBeamDeform = null;
            JProperty propScaleBeamStrength = null;
            if (part.BeamScales != null)
            {
                if (part.BeamScales.SpringScale > float.MinValue)
                    propScaleBeamSpring = new JProperty("scalebeamSpring", part.BeamScales.SpringScale.ToJBeam());
                if (part.BeamScales.DampScale > float.MinValue)
                    propScaleBeamDamp = new JProperty("scalebeamDamp", part.BeamScales.DampScale.ToJBeam());
                if (part.BeamScales.DeformScale > float.MinValue)
                    propScaleBeamDeform = new JProperty("scalebeamDeform", part.BeamScales.DeformScale.ToJBeam());
                if (part.BeamScales.StrengthScale > float.MinValue)
                    propScaleBeamStrength = new JProperty("scalebeamStrength", part.BeamScales.StrengthScale.ToJBeam());
            }

            /* Build collision values */
            JProperty propSelfCollisionThickness = null;
            JProperty propRemoteCollisionThickness = null;
            JProperty propZeroOutSelfCollision = null;
            if (part.Collisions != null)
            {
                if (part.Collisions.SelfCollisionThickness > float.MinValue)
                    propSelfCollisionThickness = new JProperty("selfCollisionThickness",
                        part.Collisions.SelfCollisionThickness.ToJBeam());
                if (part.Collisions.RemoteCollisionThickness > float.MinValue)
                    propRemoteCollisionThickness = new JProperty("remoteCollisionThickness",
                        part.Collisions.RemoteCollisionThickness.ToJBeam());
                if (part.Collisions.ZeroOutSelfCollision != DefaultBoolean.Undetermined)
                    propZeroOutSelfCollision = new JProperty("zeroOutSelfCollision",
                        bool.Parse(part.Collisions.ZeroOutSelfCollision.ToString()).ToJBeam());
            }

            /* Build ref nodes */
            var propRefNodes = BuildPartRefNodes(part);

            /* Build camera external */
            var propCameraExternal = BuildPartCameraExternal(part);

            /* Build cameras internal */
            var propCamerasInternal = BuildPartCamerasInternal(part);

            /* Build ropables */
            var propRopables = BuildPartRopables(part);

            /* Build props */
            var propProps = BuildPartProps(part);

            /* Build glow map */
            var propGlowMap = BuildPartGlowMap(part);

            /* Build flexbodies */
            var propFlexbodies = BuildPartFlexbodies(part);

            /* Build engine torque */
            var propEngineTorque = BuildPartEngineTorque(part);

            /* Build differentials */
            var propDifferentials = BuildPartDifferentials(part);

            /* Build engine properties */
            var propEngine = BuildPartEngine(part);

            /* Build ties */
            var propTies = BuildPartTies(part);

            /* Build nodes */
            var propNodes = BuildPartNodes(part);

            /* Build beams */
            var propBeams = BuildPartBeams(part);

            /* Build hydros */
            var propHydros = BuildPartHydros(part);

            /* Build thrusters */
            var propThrusters = BuildPartThrusters(part);

            /* Build hubWheels */
            var propHubWheels = BuildPartHubWheels(part);

            /* Build triangles */
            var propTriangles = BuildPartTriangles(part);

            /* Build the part */
            var partObj = new JObject();
            if (propInformation != null) partObj.Add(propInformation);
            partObj.Add(propSlotType);
            if (propSlots != null) partObj.Add(propSlots);
            if (propDragCoef != null) partObj.Add(propDragCoef);
            if (propScaleNodeWeight != null) partObj.Add(propScaleNodeWeight);
            if (propScaleBeamSpring != null) partObj.Add(propScaleBeamSpring);
            if (propScaleBeamDamp != null) partObj.Add(propScaleBeamDamp);
            if (propScaleBeamDeform != null) partObj.Add(propScaleBeamDeform);
            if (propScaleBeamStrength != null) partObj.Add(propScaleBeamStrength);
            if (propSelfCollisionThickness != null) partObj.Add(propSelfCollisionThickness);
            if (propRemoteCollisionThickness != null) partObj.Add(propRemoteCollisionThickness);
            if (propZeroOutSelfCollision != null) partObj.Add(propZeroOutSelfCollision);
            if (propRefNodes != null) partObj.Add(propRefNodes);
            if (propCameraExternal != null) partObj.Add(propCameraExternal);
            if (propCamerasInternal != null) partObj.Add(propCamerasInternal);
            if (propRopables != null) partObj.Add(propRopables);
            if (propProps != null) partObj.Add(propProps);
            if (propGlowMap != null) partObj.Add(propGlowMap);
            if (propFlexbodies != null) partObj.Add(propFlexbodies);
            if (propEngineTorque != null) partObj.Add(propEngineTorque);
            if (propEngine != null) partObj.Add(propEngine);
            if (propDifferentials != null) partObj.Add(propDifferentials);
            if (propTies != null) partObj.Add(propTies);
            if (propNodes != null) partObj.Add(propNodes);
            if (propBeams != null) partObj.Add(propBeams);
            if (propHydros != null) partObj.Add(propHydros);
            if (propThrusters != null) partObj.Add(propThrusters);
            if (propHubWheels != null) partObj.Add(propHubWheels);
            if (propTriangles != null) partObj.Add(propTriangles);

            var partProp = new JProperty(part.Name, partObj);

            /* Add part to data */
            _jbeam.Root.Add(partProp);

            #region For later... (to build a better looking jbeam)

            // TODO finish implementing patterns for jbeam indent

            /* Build patterns */
            var patBuilder = new StringBuilder(string.Format(@"{{""{0}"":{{", Name));
            var repBuilder = new StringBuilder(string.Format(@"{{\n\t""{0}"":{{", Name));

            var eid = 0;
            if (propInformation != null)
            {
                patBuilder.AppendFormat(@"""information"":{{(.*)}},");
                repBuilder.AppendFormat(@"\n\t\t""information"":{{\n\t\t\t\{0}}},", (++eid));
            }

            pattern = patBuilder.ToString();
            replacement = repBuilder.ToString();

            #endregion
        }

        private static JProperty BuildPartInformation(VehiclePart part)
        {
            var propName = new JProperty("name", part.Description);
            var propAuthors = new JProperty("authors", string.Join(",", part.Authors));

            var obj = new JObject { propName, propAuthors };
            return new JProperty("information", obj);
        }

        private static JProperty BuildPartSlots(VehiclePart part)
        {
            /* No slots */
            if (part.Slots.Count == 0)
                return null;

            var arrSlots = new JArray { new JArray { "type", "default", "description" } };
            foreach (var elemArray in part.Slots.Select(slot => slot.ToJBeam()).OfType<JArray>())
            {
                arrSlots.Add(elemArray);
            }

            return new JProperty("slots", arrSlots);
        }

        private static JProperty BuildPartDifferentials(VehiclePart part)
        {
            /* No differentials */
            if (part.Differentials.Count == 0)
                return null;

            var arrDifferentials = new JArray { new JArray { "wheelName1", "wheelName2", "type", "state", "closedTorque", "engineTorqueCoef" } };
            foreach (var elemArray in part.Differentials.Select(differential => differential.ToJBeam()).OfType<JArray>())
            {
                arrDifferentials.Add(elemArray);
            }

            return new JProperty("differentials", arrDifferentials);
        }

        private static JProperty BuildPartEngineTorque(VehiclePart part)
        {
            /* No torque values */
            if (part.EngineTorque.Count == 0)
                return null;

            var arrEngineTorque = new JArray { new JArray { "rpm", "torque" } };
            foreach (var torquePerRPM in part.EngineTorque)
            {
                arrEngineTorque.Add(new JArray { torquePerRPM.RPM, torquePerRPM.Torque });
            }

            return new JProperty("enginetorque", arrEngineTorque);
        }

        private static JProperty BuildPartEngine(VehiclePart part)
        {
            /* No engine properties */
            if (!part.Engine.HasValues)
                return null;

            var objEngine = new JObject();

            /* Configuration */
            if (part.Engine.TransmissionType != TransmissionType.Undefined)
                objEngine.Add(new JProperty("transmissionType", part.Engine.TransmissionType.ToString().Propertize().ToJBeam()));
            if (part.Engine.LowShiftDownRPM > int.MinValue)
                objEngine.Add(new JProperty("lowShiftDownRPM", part.Engine.LowShiftDownRPM.ToJBeam()));
            if (part.Engine.LowShiftUpRPM > int.MinValue)
                objEngine.Add(new JProperty("lowShiftUpRPM", part.Engine.LowShiftUpRPM.ToJBeam()));
            if (part.Engine.HighShiftDownRPM > int.MinValue)
                objEngine.Add(new JProperty("highShiftDownRPM", part.Engine.HighShiftDownRPM.ToJBeam()));
            if (part.Engine.HighShiftUpRPM > int.MinValue)
                objEngine.Add(new JProperty("highShiftUpRPM", part.Engine.HighShiftUpRPM.ToJBeam()));
            if (part.Engine.ClutchDuration > float.MinValue)
                objEngine.Add(new JProperty("clutchDuration", part.Engine.ClutchDuration.ToJBeam()));
            if (part.Engine.ViscousCoupling > int.MinValue)
                objEngine.Add(new JProperty("viscousCoupling", part.Engine.ViscousCoupling.ToJBeam()));
            if (part.Engine.LockingTorqueLimit > long.MinValue)
                objEngine.Add(new JProperty("lockingTorqueLimit", part.Engine.LockingTorqueLimit.ToJBeam()));
            if (part.Engine.EnableLocking)
                objEngine.Add(new JProperty("enableLocking", part.Engine.EnableLocking.ToJBeam()));
            if (part.Engine.IdleRPM > float.MinValue)
                objEngine.Add(new JProperty("idleRPM", part.Engine.IdleRPM.ToJBeam()));
            if (part.Engine.ShiftDownRPM > float.MinValue)
                objEngine.Add(new JProperty("shiftDownRPM", part.Engine.ShiftDownRPM.ToJBeam()));
            if (part.Engine.ShiftUpRPM > float.MinValue)
                objEngine.Add(new JProperty("shiftUpRPM", part.Engine.ShiftUpRPM.ToJBeam()));
            if (part.Engine.MaxRPM > float.MinValue)
                objEngine.Add(new JProperty("maxRPM", part.Engine.MaxRPM.ToJBeam()));
            if (part.Engine.Inertia > float.MinValue)
                objEngine.Add(new JProperty("inertia", part.Engine.Inertia.ToJBeam()));
            if (part.Engine.Friction > float.MinValue)
                objEngine.Add(new JProperty("friction", part.Engine.Friction.ToJBeam()));
            if (part.Engine.BrakingCoefRPS > float.MinValue)
                objEngine.Add(new JProperty("brakingCoefRPS", part.Engine.BrakingCoefRPS.ToJBeam()));
            if (part.Engine.BurnEfficiency > float.MinValue)
                objEngine.Add(new JProperty("burnEfficiency", part.Engine.BurnEfficiency.ToJBeam()));
            if (part.Engine.ThrottleSensitivity > float.MinValue)
                objEngine.Add(new JProperty("throttleSensitivity", part.Engine.ThrottleSensitivity.ToJBeam()));
            if (part.Engine.FuelCapacity > int.MinValue)
                objEngine.Add(new JProperty("fuelCapacity", part.Engine.FuelCapacity.ToJBeam()));
            if (part.Engine.ABSRate > int.MinValue)
                objEngine.Add(new JProperty("ABSrate", part.Engine.ABSRate.ToJBeam()));
            if (part.Engine.Differential > float.MinValue)
                objEngine.Add(new JProperty("differential", part.Engine.Differential.ToJBeam()));
            if (part.Engine.AxleFriction > float.MinValue)
                objEngine.Add(new JProperty("axleFriction", part.Engine.AxleFriction.ToJBeam()));

            /* Breakable driveshafts */
            if (part.Engine.BreakableDriveshafts.Count > 0)
            {
                var arrDriveshafts = new JArray(part.Engine.BreakableDriveshafts);
                var propDriveshafts = new JProperty("onBeamBreakDisableDriveshaft", arrDriveshafts);
                objEngine.Add(propDriveshafts);
            }

            /* Water damage groups */
            if (part.Engine.WaterDamageGroups.Count > 0)
            {
                var arrWaterDamage = new JArray(part.Engine.WaterDamageGroups);
                var propWaterDamage = new JProperty("[engineGroup]:", arrWaterDamage);
                var objWaterDamage = new JObject(propWaterDamage);
                var propWaterDamageGroups = new JProperty("waterDamage", objWaterDamage);
                objEngine.Add(propWaterDamageGroups);
            }

            /* Gears */
            if (part.Engine.Gears.Count > 0)
            {
                var arrGears = new JArray(part.Engine.Gears.Select(gear => gear.Value));
                var propGears = new JProperty("gears", arrGears);
                objEngine.Add(propGears);
            }

            /* Fuel tank beams */
            if (part.Engine.FuelTankBeams.Count > 0)
            {
                var arrFuelTankBeams = new JArray(part.Engine.FuelTankBeams);
                var propFuelTankBeams = new JProperty("fuelTankBeams", arrFuelTankBeams);
                objEngine.Add(propFuelTankBeams);
            }

            return objEngine.HasValues ? new JProperty("engine", objEngine) : null;
        }

        private static JProperty BuildPartNodes(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.NodeSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id", "posX", "posY", "posZ" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartBeams(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.BeamSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id1:", "id2:" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartFlexbodies(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.FlexbodySection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "mesh", "[group]:", "nonFlexMaterials" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartRefNodes(VehiclePart part)
        {
            /* No ref nodes */
            if (string.IsNullOrEmpty(part.RefNodes.RefNodeId))
                return null;

            /* Build the ref nodes array */
            var arrRefNodes = new JArray
            {
                new JArray { "ref:", "back:", "left:", "up:" },
                new JArray { part.RefNodes.RefNodeId, part.RefNodes.BackNodeId, part.RefNodes.LeftNodeId, part.RefNodes.UpNodeId }
            };

            return new JProperty("refNodes", arrRefNodes);
        }

        private static JProperty BuildPartCameraExternal(VehiclePart part)
        {
            /* External camera not defined */
            var cam = part.CameraExternal;
            if (!cam.IsDefined)
                return null;

            /* Init object */
            var obj = new JObject();

            /* Distance */
            if (cam.Distance > float.MinValue)
            {
                obj.Add(new JProperty("distance", cam.Distance));
            }

            /* Distance min */
            if (cam.DistanceMin > float.MinValue)
            {
                obj.Add(new JProperty("distanceMin", cam.DistanceMin));
            }

            /* Offset */
            if (cam.Offset.IsValid)
            {
                var objOffset = new JObject
                {
                    new JProperty("x", cam.Offset.X),
                    new JProperty("y", cam.Offset.Y),
                    new JProperty("z", cam.Offset.Z)
                };
                obj.Add(new JProperty("offset", objOffset));
            }

            /* FOV */
            if (cam.FOV > float.MinValue)
            {
                obj.Add(new JProperty("fov", cam.FOV));
            }

            /* Return camera external */
            return obj.HasValues ? new JProperty("cameraExternal", obj) : null;
        }

        private static JProperty BuildPartCamerasInternal(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.InternalCameraSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "x", "y", "z", "fov", "id1:", "id2:", "id3:", "id4:", "id5:", "id6:" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartRopables(VehiclePart part)
        {
            /* No ropables */
            if (part.Ropables.Count == 0)
                return null;

            var arrRopables = new JArray { new JArray { "id:", "group", "multilock" } };
            for (var index = 0; index < part.Ropables.Count; index++)
            {
                /* Get camera */
                var ropable = part.Ropables[index];
                if (ropable == null) continue;

                /* Add ropable */
                arrRopables.Add(new JArray
                {
                    ropable.NodeId,
                    ropable.Group,
                    ropable.MultiLock
                });
            }
            return new JProperty("ropables", arrRopables);
        }

        private static JProperty BuildPartProps(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.PropSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "func", "mesh", "idRef:", "idX:", "idY:", "baseRotation", "rotation", "translation", "min", "max", "offset", "multiplier" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = new JArray
                {
                    elem.Func,
                    elem.Mesh,
                    elem.NodeIdRef,
                    elem.NodeIdX,
                    elem.NodeIdY,
                    elem.BaseRotation.ToJBeam(),
                    elem.Rotation.ToJBeam(),
                    elem.Translation.ToJBeam(),
                    elem.Min,
                    elem.Max,
                    elem.Offset,
                    elem.Multiplier
                };
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartGlowMap(VehiclePart part)
        {
            /* No glow materials */
            if (part.GlowMap.Count == 0)
                return null;

            var objGlowMap = new JObject();
            foreach (var glowMat in part.GlowMap)
            {
                JProperty propSimpleFunction = null;
                JProperty propOff = null;
                JProperty propOn = null;
                JProperty propOnIntense = null;

                if (!string.IsNullOrEmpty(glowMat.OffMaterial))
                    propOff = new JProperty("off", glowMat.OffMaterial);
                if (!string.IsNullOrEmpty(glowMat.OnMaterial))
                    propOn = new JProperty("on", glowMat.OnMaterial);

                /* Custom material */
                if (glowMat is CustomGlowMaterial)
                {
                    if (!string.IsNullOrEmpty(((CustomGlowMaterial)glowMat).OnIntenseMaterial))
                        propOnIntense = new JProperty("on_intense", ((CustomGlowMaterial)glowMat).OnIntenseMaterial);

                    if (((CustomGlowMaterial)glowMat).SimpleFunction.Count > 0)
                    {
                        var objSimpleFunction = new JObject();
                        foreach (var sfct in ((CustomGlowMaterial)glowMat).SimpleFunction)
                        {
                            objSimpleFunction.Add(new JProperty(sfct.Name, sfct.Value.ToJBeam()));
                        }
                        if (objSimpleFunction.HasValues)
                            propSimpleFunction = new JProperty("simpleFunction", objSimpleFunction);
                    }
                }

                /* Global material */
                else if (glowMat is GlobalGlowMaterial)
                {
                    if (!string.IsNullOrEmpty(((GlobalGlowMaterial)glowMat).SimpleFunction))
                        propSimpleFunction = new JProperty("simpleFunction", ((GlobalGlowMaterial)glowMat).SimpleFunction);
                }

                /* Store properties */
                var objProperties = new JObject();
                if (propSimpleFunction != null)
                    objProperties.Add(propSimpleFunction);
                if (propOff != null)
                    objProperties.Add(propOff);
                if (propOn != null)
                    objProperties.Add(propOn);
                if (propOnIntense != null)
                    objProperties.Add(propOnIntense);

                /* Define glow mapping */
                if (objProperties.HasValues)
                {
                    objGlowMap.Add(new JProperty(glowMat.Name, objProperties));
                }
            }

            if (objGlowMap.HasValues)
                return new JProperty("glowMap", objGlowMap);
            else
                return null;
        }

        private static JProperty BuildPartTriangles(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.TriangleSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id1:", "id2:", "id3:" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = new JArray { elem.NodeId1, elem.NodeId2, elem.NodeId3 };
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartHydros(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.HydroSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id1:", "id2:" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartThrusters(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.ThrusterSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id1:", "id2:" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartHubWheels(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.HubWheelSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "name", "group", "node1:", "node2:", "nodeS", "nodeArm:", "wheelDir" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        private static JProperty BuildPartTies(VehiclePart part)
        {
            /* Empty section, skip it */
            var section = part.TieSection;
            if (section.IsEmpty) return null;

            /* Close grouping properties */
            GroupHelper.CloseGroupingProperties(section);

            var array = new JArray { new JArray { "id1:", "maxLen", "rate", "short", "long", "group", "maxforce" } };
            for (var index = 0; index <= section.HighIndex; index++)
            {
                /* Variables */
                var groupProps = new JObject();
                var elem = section[index];
                var properties = section.Properties[index].ToSafeList();

                /* Create group properties */
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Group))
                    groupProps.Add(prop.ToJProperty());

                /* Add group properties */
                if (groupProps.Count > 0)
                    array.Add(groupProps);

                /* Add element */
                if (elem == null) continue;
                var elemArray = elem.ToJBeam() as JArray;
                if (elemArray == null) continue;
                array.Add(elemArray);

                /* Create inline properties */
                var singleProps = new JObject();
                foreach (var prop in properties.Where(prop => prop.Use == ElementPropertyUse.Single))
                    singleProps.Add(prop.ToJProperty());

                /* Add inline props */
                if (singleProps.Count == 0) continue;
                elemArray.Add(singleProps);
            }

            /* Return */
            return new JProperty(section.Name, array);
        }

        #endregion

        #region Extraction

        /// <summary>Extracts a vehicle part and stores it</summary>
        /// <param name="token"></param>
        private void ExtractPart(JProperty token)
        {
            /* Create the part for this token */
            if (!AddPart(token.Name)) return;

            /* Build the vehicle part structure, different groups (nodes, beams, ...) - Should only have objects on this level */
            var part = this[token.Name];
            foreach (var groupObj in token.Children<JObject>())
            {
                foreach (var group in groupObj.Properties())
                {
                    switch (group.Name.ToUpperInvariant())
                    {
                        case "INFORMATION":
                            ExtractPartInformation(group, part);
                            break;

                        case "SLOTTYPE":
                            part.SlotType = group.Value.FromJBeam<string>();
                            break;

                        case "SLOTS":
                            ExtractPartSlots(group, part);
                            break;

                        case "FLEXBODIES":
                            ExtractPartFlexbodies(group, part);
                            break;

                        case "NODES":
                            ExtractPartNodes(group, part);
                            break;

                        case "BEAMS":
                            ExtractPartBeams(group, part);
                            break;

                        case "THRUSTERS":
                            ExtractPartThrusters(group, part);
                            break;

                        case "DRAGCOEF":
                            part.DragCoef = group.Value.FromJBeam<float>();
                            break;

                        case "SCALEBEAMSPRING":
                            Parent.BeamScales.SpringScale = group.Value.FromJBeam<float>();
                            part.BeamScales.SpringScale = group.Value.FromJBeam<float>();
                            break;

                        case "SCALEBEAMDAMP":
                            Parent.BeamScales.DampScale = group.Value.FromJBeam<float>();
                            part.BeamScales.DampScale = group.Value.FromJBeam<float>();
                            break;

                        case "SCALEBEAMDEFORM":
                            Parent.BeamScales.DeformScale = group.Value.FromJBeam<float>();
                            part.BeamScales.DeformScale = group.Value.FromJBeam<float>();
                            break;

                        case "SCALEBEAMSTRENGTH":
                            Parent.BeamScales.StrengthScale = group.Value.FromJBeam<float>();
                            part.BeamScales.StrengthScale = group.Value.FromJBeam<float>();
                            break;

                        case "SCALENODEWEIGHT":
                            Parent.NodeScales.WeightScale = group.Value.FromJBeam<float>();
                            part.NodeScales.WeightScale = group.Value.FromJBeam<float>();
                            break;

                        case "SELFCOLLISIONTHICKNESS":
                            Parent.Collisions.SelfCollisionThickness = group.Value.FromJBeam<float>();
                            part.Collisions.SelfCollisionThickness = group.Value.FromJBeam<float>();
                            break;

                        case "REMOTECOLLISIONTHICKNESS":
                            Parent.Collisions.RemoteCollisionThickness = group.Value.FromJBeam<float>();
                            part.Collisions.RemoteCollisionThickness = group.Value.FromJBeam<float>();
                            break;

                        case "ZEROOUTSELFCOLLISION":
                            Parent.Collisions.ZeroOutSelfCollision = (DefaultBoolean)((bool)group.Value ? 1 : 0);
                            part.Collisions.ZeroOutSelfCollision = (DefaultBoolean)((bool)group.Value ? 1 : 0);
                            break;

                        case "REFNODES":
                            ExtractPartRefNodes(group, part);
                            break;

                        case "CAMERAEXTERNAL":
                            ExtractPartCameraExternal(group, part);
                            break;

                        case "CAMERASINTERNAL":
                            ExtractPartCamerasInternal(group, part);
                            break;

                        case "ROPABLES":
                            ExtractPartRopables(group, part);
                            break;

                        case "PROPS":
                            ExtractPartProps(group, part);
                            break;

                        case "GLOWMAP":
                            ExtractPartGlowMap(group, part);
                            break;

                        case "ENGINE":
                            ExtractPartEngine(group, part);
                            break;

                        case "ENGINETORQUE":
                            ExtractPartEngineTorque(group, part);
                            break;

                        case "DIFFERENTIALS":
                            ExtractPartDifferentials(group, part);
                            break;

                        case "TRIANGLES":
                            ExtractPartTriangles(group, part);
                            break;

                        case "HUBWHEELS":
                            ExtractPartHubWheels(group, part);
                            break;

                        case "HYDROS":
                            ExtractPartHydros(group, part);
                            break;

                        case "TIES":
                            ExtractPartTies(group, part);
                            break;
                    }
                }

                /* GroupList must remain unique so skip any other structure */
                break;
            }
        }

        /// <summary>Builds the Information section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartInformation(JProperty token, VehiclePart part)
        {
            foreach (var groupObj in token.Children<JObject>())
            {
                foreach (var group in groupObj.Properties())
                {
                    switch (group.Name.ToUpperInvariant())
                    {
                        case "NAME":
                            part.Description = (string)(group.Value as JValue).Value;
                            break;

                        case "AUTHORS":
                            part.AddAuthors(((string)(group.Value as JValue).Value).Split(','));
                            break;
                    }
                }

                /* Single information instance supported */
                break;
            }
        }

        /// <summary>Builds the Slots section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartSlots(JProperty token, VehiclePart part)
        {
            foreach (var slotArray in token.Children<JArray>())
            {
                foreach (var slotElem in slotArray.Skip(1))
                {
                    part.Slots.Add(
                        slotElem.Value<string>(0),
                        (string)(slotElem.ElementAtOrDefault(2) ?? slotElem.Value<string>(1)),
                        part,
                        slotElem.Value<string>(1)
                    );
                }
            }
        }

        /// <summary>Builds the Nodes section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartNodes(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.NodeSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Node>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Builds the Beams section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartBeams(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.BeamSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements & inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID1:")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Beam>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Node properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Builds the Flexbodies section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartFlexbodies(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.FlexbodySection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements & inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("MESH")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Flexbody>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Node properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Builds the ref nodes section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartRefNodes(JProperty token, VehiclePart part)
        {
            foreach (var gArray in token.Children<JArray>())
            {
                foreach (var vArray in gArray.Children<JArray>())
                {
                    /* Values */
                    var refNodeId = vArray.Value<string>(0);
                    var backNodeId = vArray.Value<string>(1);
                    var leftNodeId = vArray.Value<string>(2);
                    var upNodeId = vArray.Value<string>(3);

                    /* Skip information line */
                    if (refNodeId.ToUpperInvariant().Equals("REF:")) continue;

                    /* Define or update ref nodes */
                    part.RefNodes.RefNodeId = refNodeId;
                    part.RefNodes.BackNodeId = backNodeId;
                    part.RefNodes.LeftNodeId = leftNodeId;
                    part.RefNodes.UpNodeId = upNodeId;
                }

                /* Only one ref nodes section allowed */
                break;
            }
        }

        /// <summary>Builds the external camera section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartCameraExternal(JProperty token, VehiclePart part)
        {
            var section = token.Value as JObject;
            if (section == null) return;

            foreach (var prop in section.Children<JProperty>())
            {
                switch (prop.Name.ToUpperInvariant())
                {
                    case "DISTANCE":
                        part.CameraExternal.Distance = (float)prop.Value;
                        break;
                    case "DISTANCEMIN":
                        part.CameraExternal.DistanceMin = (float)prop.Value;
                        break;
                    case "OFFSET":
                        part.CameraExternal.Offset = new Float3(
                            prop.Value.Value<float>("x"),
                            prop.Value.Value<float>("y"),
                            prop.Value.Value<float>("z")
                        );
                        break;
                    case "FOV":
                        part.CameraExternal.FOV = (float)prop.Value;
                        break;
                }
            }
        }

        /// <summary>Extracts the internal cameras section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartCamerasInternal(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.InternalCameraSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("X")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<InternalCamera>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Builds the ropables section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartRopables(JProperty token, VehiclePart part)
        {
            foreach (var gArray in token.Children<JArray>())
            {
                foreach (var vArray in gArray.Children<JArray>())
                {
                    /* Skip definition line */
                    var nodeId = vArray.Value<string>(0);
                    if (nodeId.ToUpperInvariant().Equals("ID:")) continue;

                    /* Get other values */
                    var group = vArray.Value<string>(1);
                    var multiLock = vArray.Value<bool>(2);

                    /* Add ropable */
                    part.Ropables.UpdateAdd(nodeId, group, multiLock);
                }

                /* Only one ropables section allowed */
                break;
            }
        }

        /// <summary>Builds the glow map section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartGlowMap(JProperty token, VehiclePart part)
        {
            /* Get the section */
            var objGlowMap = token.Value as JObject;
            if (objGlowMap == null) return;

            foreach (var prop in objGlowMap.Properties())
            {
                var objGlowMaterial = prop.Value as JObject;
                if (objGlowMaterial == null) continue;

                /* Extract properties */
                var name = prop.Name;
                var objSimpleFunction = objGlowMaterial.GetValue("simpleFunction") as JObject;
                var jValueSimpleFunction = objGlowMaterial.GetValue("simpleFunction") as JValue;
                var jValueOff = objGlowMaterial.GetValue("off") as JValue;
                var jValueOn = objGlowMaterial.GetValue("on") as JValue;
                var jValueOnIntense = objGlowMaterial.GetValue("on_intense") as JValue;

                /* Extract common values */
                var materialOff = jValueOff != null ? jValueOff.ToString() : string.Empty;
                var materialOn = jValueOn != null ? jValueOn.ToString() : string.Empty;
                var materialOnIntense = jValueOnIntense != null ? jValueOnIntense.ToString() : string.Empty;

                /* Global material */
                GlowMaterial mat = null;
                if (objSimpleFunction == null && jValueSimpleFunction != null)
                {
                    var simpleFunction = jValueSimpleFunction.ToString();
                    mat = new GlobalGlowMaterial(name, materialOn, materialOff, simpleFunction);
                }

                /* Custom material */
                else if (objSimpleFunction != null && jValueSimpleFunction == null)
                {
                    mat = new CustomGlowMaterial(name, materialOn, materialOff, materialOnIntense);
                    foreach (var propMat in objSimpleFunction.Properties())
                    {
                        ((CustomGlowMaterial)mat).SimpleFunction.Add(propMat.Name, (float)propMat.Value);
                    }
                }

                /* Add glow material to glowMap */
                if (mat != null)
                {
                    part.GlowMap.UpdateAdd(mat);
                }
            }
        }

        /// <summary>Extracts the engine section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartEngine(JProperty token, VehiclePart part)
        {
            /* Get the property list */
            var obj = token.Value as JObject;
            if (obj == null) return;

            foreach (var prop in obj.Properties())
            {
                switch (prop.Name.ToUpperInvariant())
                {
                    case "TRANSMISSIONTYPE":
                        part.Engine.TransmissionType = (TransmissionType)Enum.Parse(typeof(TransmissionType), (string)prop.Value, true);
                        break;
                    case "LOWSHIFTDOWNRPM":
                        part.Engine.LowShiftDownRPM = (float)prop.Value;
                        break;
                    case "LOWSHIFTUPRPM":
                        part.Engine.LowShiftUpRPM = (float)prop.Value;
                        break;
                    case "HIGHSHIFTDOWNRPM":
                        part.Engine.HighShiftDownRPM = (float)prop.Value;
                        break;
                    case "HIGHSHIFTUPRPM":
                        part.Engine.HighShiftUpRPM = (float)prop.Value;
                        break;
                    case "CLUTCHDURATION":
                        part.Engine.ClutchDuration = (float)prop.Value;
                        break;
                    case "VISCOUSCOUPLING":
                        part.Engine.ViscousCoupling = (int)prop.Value;
                        break;
                    case "LOCKINGTORQUELIMIT":
                        part.Engine.LockingTorqueLimit = (long)prop.Value;
                        break;
                    case "ENABLELOCKING":
                        part.Engine.EnableLocking = (bool)prop.Value;
                        break;
                    case "IDLERPM":
                        part.Engine.IdleRPM = (float)prop.Value;
                        break;
                    case "SHIFTDOWNRPM":
                        part.Engine.ShiftDownRPM = (float)prop.Value;
                        break;
                    case "SHIFTUPRPM":
                        part.Engine.ShiftUpRPM = (float)prop.Value;
                        break;
                    case "MAXRPM":
                        part.Engine.MaxRPM = (float)prop.Value;
                        break;
                    case "INERTIA":
                        part.Engine.Inertia = (float)prop.Value;
                        break;
                    case "FRICTION":
                        part.Engine.Friction = (float)prop.Value;
                        break;
                    case "BRAKINGCOEFRPS":
                        part.Engine.BrakingCoefRPS = (float)prop.Value;
                        break;
                    case "BURNEFFICIENCY":
                        part.Engine.BurnEfficiency = (float)prop.Value;
                        break;
                    case "THROTTLESENSITIVITY":
                        part.Engine.ThrottleSensitivity = (float)prop.Value;
                        break;
                    case "FUELCAPACITY":
                        part.Engine.FuelCapacity = (int)prop.Value;
                        break;
                    case "ABSRATE":
                        part.Engine.ABSRate = (float)prop.Value;
                        break;
                    case "DIFFERENTIAL":
                        part.Engine.Differential = (float)prop.Value;
                        break;
                    case "AXLEFRICTION":
                        part.Engine.AxleFriction = (float)prop.Value;
                        break;
                    case "ONBEAMBREAKDISABLEDRIVESHAFT":
                        var arrDriveshafts = prop.Value as JArray;
                        if (arrDriveshafts == null) continue;

                        part.Engine.BreakableDriveshafts.AddRange(arrDriveshafts.Values<string>());
                        break;
                    case "WATERDAMAGE":
                        var objWaterDamage = prop.Value as JObject;
                        if (objWaterDamage == null) continue;

                        var propWaterDamage = objWaterDamage.Property("[engineGroup]:");
                        if (propWaterDamage == null) continue;

                        var arrWaterDamage = propWaterDamage.Value as JArray;
                        if (arrWaterDamage == null) continue;

                        part.Engine.WaterDamageGroups.AddRange(arrWaterDamage.Values<string>());
                        break;
                    case "GEARS":
                        var arrGears = prop.Value as JArray;
                        if (arrGears == null) continue;

                        part.Engine.Gears.AddRange(arrGears.Values<float>()
                                .Select(val => new Gear((GearPosition)(part.Engine.Gears.Count - 1), val)));
                        break;
                    case "FUELTANKBEAMS":
                        var arrFuelTankBeams = prop.Value as JArray;
                        if (arrFuelTankBeams == null) continue;

                        part.Engine.FuelTankBeams.AddRange(arrFuelTankBeams.Values<string>());
                        break;
                }
            }
        }

        /// <summary>Extracts the engine torque section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartEngineTorque(JProperty token, VehiclePart part)
        {
            var arrEngineTorque = token.Value as JArray;
            if (arrEngineTorque == null) return;

            foreach (var arrTorquePerRPM in arrEngineTorque.Children<JArray>())
            {
                var rpm = arrTorquePerRPM[0];
                var torque = arrTorquePerRPM[1];

                /* Skip definition line */
                if (rpm.ToStringOrEmpty().ToUpperInvariant().Equals("RPM") &&
                    torque.ToStringOrEmpty().ToUpperInvariant().Equals("TORQUE"))
                {
                    continue;
                }

                /* Save torque per RPM */
                part.EngineTorque.Add((int)rpm, (int)torque);
            }
        }

        /// <summary>Extracts the differentials section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartDifferentials(JProperty token, VehiclePart part)
        {
            var arrDifferentials = token.Value as JArray;
            if (arrDifferentials == null) return;

            foreach (var arrDifferential in arrDifferentials.Children<JArray>())
            {
                /* Skip definition line */
                if (arrDifferential[0].ToStringOrEmpty().ToUpperInvariant().Equals("WHEELNAME1"))
                    continue;

                var differential = new Differential(
                    arrDifferential[0].ToStringOrEmpty(),
                    arrDifferential[1].ToStringOrEmpty()
                )
                {
                    Type = (DifferentialType)Enum.Parse(typeof(DifferentialType), arrDifferential[2].ToStringOrEmpty(), true),
                    State = (DifferentialType)Enum.Parse(typeof(DifferentialType), arrDifferential[3].ToStringOrEmpty(), true),
                    ClosedTorque = arrDifferential.Value<int>(4),
                    EngineTorqueCoef = arrDifferential.Value<float>(5)
                };

                part.Differentials.Add(differential);
            }
        }

        /// <summary>Extracts the props section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartProps(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.PropSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements & inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("FUNC")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Prop>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }
                        break;

                    /* Group properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Extracts the triangles section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartTriangles(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.TriangleSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID1:")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Triangle>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Extracts the hydros section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartHydros(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.HydroSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID1:")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Hydro>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Extracts the hubWheels section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartHubWheels(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.HubWheelSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToStringOrEmpty().ToUpperInvariant().Equals("NAME") ||
                            childArray[0].ToStringOrEmpty().ToUpperInvariant().Equals("RADIUS"))
                            continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<HubWheel>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Extracts the ties section</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartTies(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.TieSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements and inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID1:")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Tie>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Element properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        /// <summary>Extracts the Thrusters section of the vehicle part</summary>
        /// <param name="token"></param>
        /// <param name="part"></param>
        private static void ExtractPartThrusters(JProperty token, VehiclePart part)
        {
            var array = token.Value as JArray;
            if (array == null) return;

            var section = part.ThrusterSection;
            foreach (var child in array.Children())
            {
                switch (child.Type)
                {
                    /* Actual elements & inline properties */
                    case JTokenType.Array:
                        var childArray = (JArray)child;
                        if (childArray[0].ToString().ToUpperInvariant().Equals("ID1:")) continue;

                        /* Add new element */
                        try { section.AddElement(VehicleElement.NewFromJBeam<Thruster>(child)); }
                        catch (DriveException ex)
                        {
                            part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                        }

                        /* Inline properties */
                        foreach (var prop in childArray.Children<JObject>().SelectMany(obj => obj.Children<JProperty>()))
                        {
                            try { section.SetPropertyValue(section.HighIndex, prop, ElementPropertyUse.Single); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;

                    /* Node properties */
                    case JTokenType.Object:
                        foreach (var prop in ((JObject)child).Properties())
                        {
                            try { section.SetPropertyValue(section.Count, prop, ElementPropertyUse.Group); }
                            catch (UnknownPropertyException ex)
                            {
                                ex.ObjectName = part.Description;
                                ex.Location = section.Name;
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                            catch (DriveException ex)
                            {
                                part.File.Parent.ErrorManager.AddError(part.Description, ex.Message);
                            }
                        }
                        break;
                }
            }
        }

        #endregion

        #region Other

        /// <summary>Gets the patterns to use to indent the output when compressed</summary>
        /// <returns></returns>
        private string[] GetSavePatterns()
        {
            var patterns = new List<string>();
            var mainPattern = new StringBuilder(@"\{");


            patterns.Add(@"\],\[");
            patterns.Add(@"\],\n\t\t\t\[");

            patterns.Add(@"\],\{");
            patterns.Add(@"\],\n\t\t\t\{");

            patterns.Add(@"\},\[");
            patterns.Add(@"\},\n\t\t\t\[");

            return patterns.ToArray();
        }

        #endregion

        #endregion

        #region KeyedCollection

        protected override string GetKeyForItem(VehiclePart item)
        {
            return item.Name;
        }

        #endregion
    }
}
